"""
TOPSIS法(优劣解距离法)
"""

import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler

# 显示所有列
pd.set_option('display.max_columns', None)
# 显示所有行
pd.set_option('display.max_rows', None)
# 设置value的显示长度为100，默认为50
pd.set_option('max_colwidth', 100)


class TOPSIS:
    """C.L.Hwang 和 K.Yoon 于1981年首次提出 TOPSIS 
    (Technique for Order Preference by Similarity to an Ideal Solution)。TOPSIS 法是一种常
    用的组内综合评价方法，能充分利用原始数据的信息，其结果能精确地反映各评价方案之间的差距。基本
    过程为基于归一化后的原始数据矩阵，采用余弦法找出有限方案中的最优方案和最劣方案，然后分别计算
    各评价对象与最优方案和最劣方案间的距离，获得各评价对象与最优方案的相对接近程度，以此作为评价
    优劣的依据。该方法对数据分布及样本含量没有严格限制，数据计算简单易行。
    """

    def __init__(self):
        pass

    def direction_normalize(self, data, type='', lb=None, ub=None, lb_star=None, ub_star=None):
        """指标属性同向化，一般选择指标正向化

        TOPSIS 法使用距离尺度来度量样本差距，使用距离尺度就需要对指标属性进行同向化处理（若一个维度的数据越大越好，另一个维度的数据越小越好，会造成尺度混乱）。通常采用成本型指标向效益型指标转化（即数值越大评价越高，事实上几乎所有的评价方法都需要进行转化），此外，如果需要使用雷达图进行展示，建议此处将所有数据都变成正数。

        Parameters
        ----------
        data : 原始数据

        type : 可选'cost'（极小型指标）/'med'（中间型指标）/'interval'(区间型指标)

        # 以下参数仅在type为'interval'时有效

        [lb, ub] : 指标的最佳稳定区间

        Returns
        -------
        data_norm : 同向化后的数据
        """
        M, m = data.max(), data.min()

        def normalization(data):
            if 'cost' == type:  # 极小型指标：期望指标值越小越好
                return (M - data) / (M - m)
            elif 'med' == type:  # 中间型指标：期望指标值既不要太大也不要太小，适当取中间值最好
                if data <= m or data >= M:
                    return 0
                elif m < data < (m + M) / 2:
                    return 2 * (data - m) / (M - m)
                elif (m + M) / 2 <= data < M:
                    return 2 * (M - data) / (M - m)
            elif 'interval' == type:  # 区间型指标：期望指标的取值最好落在某一个确定的区间最好
                if lb <= data <= ub:
                    return 1
                elif data <= m or data >= M:
                    return 0
                elif ub < data < M:
                    return 1 - (data - ub) / (M - ub)
                elif m < data < lb:
                    return 1 - (lb - data) / (lb - m)
            else:
                print("There is no type called {}!".format(type))
                return data

        return list(map(normalization, data))

    def entropy_weight(self, data):
        """基于信息论的熵值法（数据差异程度）

        基于信息论的熵值法是根据各指标所含信息有序程度的差异性来确定指标权重的客观赋权方法，仅依
        赖于数据本身的离散程度。熵用于度量不确定性，指标的离散程度越大（不确定性越大）则熵值越大，
        表明指标值提供的信息量越多，则该指标的权重也应越大。
        """
        data = np.array(data)
        # 归一化: 对原始数据矩阵按列进行归一化处理（注意，这里不能是同向化后的矩阵，会损失原始信息），
        # 归一化方法不唯一，以下提供最简单的比值归一化
        # P = data / data.sum(axis=0)
        P = StandardScaler().fit_transform(data)  # z 分数变换
        # 计算各指标的熵值
        E = np.nansum(-P * np.log(P) / np.log(len(data)), axis=0)
        # 计算各指标的权系数
        return (1 - E) / (1 - E).sum()

    def run(self, data, weight=None):
        """TOPSIS法算法

        Parameters
        ----------
        data : 原始数据
        weight : 权系数, 默认使用熵权法定权. 也可以传入指定权重列表. 
        """
        # 构造归一化初始矩阵
        data = data / np.sqrt((data ** 2).sum())
        # 确定最优方案和最劣方案
        Z = pd.DataFrame([data.min(), data.max()], index=['负理想解', '正理想解'])
        weight = self.entropy_weight(
            data) if weight is None else np.array(weight)  # 设置权系数
        result = data.copy()
        # 计算各评价对象与最优方案、最劣方案的接近程度
        result['正理想解'] = np.sqrt(
            ((data - Z.loc['正理想解']) ** 2 * weight).sum(axis=1))
        result['负理想解'] = np.sqrt(
            ((data - Z.loc['负理想解']) ** 2 * weight).sum(axis=1))
        # 计算各评价对象与最优方案的贴近程度
        result['综合得分指数'] = result['负理想解'] / (result['负理想解'] + result['正理想解'])
        # 根据得分大小进行排序，给出评价结果
        result['排序'] = result.rank(ascending=False)['综合得分指数']

        return result, Z, weight


class AHP:
    """层次分析法是一种定性与定量相结合的决策分析方法，通过判断各衡量指标的相对重要程度，进而得到
    决策方案中每个指标的权重，熵权法是基于数据的定权方法，而层次分析法是基于经验的定权方法。
    注意：层次分析法所提到的特征向量、特征值并不是矩阵中的概念，是重新定义的。

    步骤：
        Step 1: 构造指标成对比较矩阵.

        Step 2: 计算矩阵的特征值和特征向量.
                (i) 先对成对比矩阵A1进行列归一化，即每个元素都除以当前列元素的和 
                (ii) 将归一化矩阵按行求和，得到每一行的“特征向量” 
                (iii) 将特征向量按列归一化，得到“指标权重”
                (iv) 成对比矩阵的每一列乘以“指标权重”中对应位置的元素得到新矩阵A2
                (v) 将A2矩阵按行求和，得到矩阵A3
                (vi) 将矩阵A3进行标准化、并对所有元素求和，得到特征值

        Step 3: 判断成对比较矩阵的一致性.(一致性指标CI和一致性比率CR)

    """

    def __init__(self):
        self.RI = {1: 0,
                   2: 0,
                   3: 0.58,
                   4: 0.90,
                   5: 1.12,
                   6: 1.24,
                   7: 1.32,
                   8: 1.41,
                   9: 1.45,
                   10: 1.49,
                   11: 1.51}

    def run(self, data):
        """AHQ算法"""

        data = np.array(data)
        m = len(data)

        # 计算特征向量
        weight = (data / data.sum(axis=0)).sum(axis=1) / m

        # 计算特征值
        Lambda = sum((weight * data).sum(axis=1) / (m * weight))

        # 判断一致性
        CI = (Lambda - m) / (m - 1)
        CR = CI / self.RI[m]

        if CR < 0.1:
            print(f'最大特征值：lambda = {Lambda}')
            print(f'特征向量：weight = {weight}')
            print(
                f'\nCI = {round(CI,2)}, RI = {self.RI[m]} \nCR = CI/RI = {round(CR,2)} < 0.1，通过一致性检验')
            return weight
        else:
            print(
                f'\nCI = {round(CI,2)}, RI = {self.RI[m]} \nCR = CI/RI = {round(CR,2)} >= 0.1，不满足一致性')


topis = TOPSIS()
data = pd.read_csv('data/river.csv', delimiter=',').drop(['河流'], axis=1)
data, columns = data.values, data.columns
data[:, 1] = topis.direction_normalize(data[:, 1], type='med')  # PH值越接近7越好
data[:, 2] = topis.direction_normalize(data[:, 3], type='cost')  # 细菌总数越少越好
data[:, 3] = topis.direction_normalize(
    data[:, 3], type='interval', lb=10, ub=20)  # 植物性营养物量介于10-20之间最佳，超过20或低于10均不好
data_normalized = pd.DataFrame(data, columns=columns)
out = topis.run(data_normalized, weight=[0.2, 0.3, 0.4, 0.1])
print(out)

ahp = AHP()
A = [[1, 4, 2, 8, 2], [1/4, 1, 1/2, 2, 1/2], [1/2, 2, 1, 4, 1],
     [1/8, 1/2, 1/4, 1, 1/4], [1/2, 2, 1, 4, 1]]
ahp.run(A)
